<?php

	namespace org\tekuna\core\context;

	use \BadMethodCallException;

	use org\tekuna\base\Tekuna;
	use org\tekuna\core\context\ContextException;
	use org\tekuna\core\util\FormattingUtil;


	/**
	 * This is the class for the application context. The context contains a number of objects that can be
	 * set and retrieved. It provides some magic convenience method to allow nicer object interactions.
	 *
	 * $objContext -> setObject('nameOfObject', $objInstance);
	 * can be written as
	 * $objContext -> setNameOfObject($objInstance);
	 *
	 * The same applies for setters and exicence checks (see below).
	 */
	class Context {

		private
			$objLogger = NULL,
			$arrObjects = array(),
			$arrInitializedObjects = array();


		/**
		 * Construct a new Context. This method does nothing than initializing the logger.
		 */
	    public function __construct() {

	    	$this -> objLogger = Tekuna :: getLogger(__CLASS__);
	    }


	    /**
	     * This is the magic method that allows nice convenience method calls on the context. The following
	     * redirects are made:
	     *
	     * set... -> setObject
	     * get... -> getObject
	     * has... -> hasObject
	     * remove... -> removeObject
	     *
	     * @param string $sMethodName the called method name
	     * @param array $arrMethodArguments the list of arguments
	     * @throws BadMethodCallException if the method does not start with 'get', 'set', 'has' or 'remove'
	     */
	    public function __call($sMethodName, array $arrMethodArguments) {

	    	// call setObject if method starts with 'set'
	    	if (substr($sMethodName, 0, 3) == 'set') {

	    		if (count($arrMethodArguments) > 1) {

	    			throw new BadMethodCallException("The method '$sMethodName' can only be used with zero or one parameter.");
	    		}

	    		if (isset($arrMethodArguments[0])) {

	    			$this -> setObject(substr($sMethodName, 3), $arrMethodArguments[0]);
	    		}
	    		else {

	    			$this -> setObject(substr($sMethodName, 3), null);
	    		}

	    		return;
	    	}

	    	// call getObject if method starts with 'get'
	    	if (substr($sMethodName, 0, 3) == 'get') {

	    		return $this -> getObject(substr($sMethodName, 3));
	    	}

	    	// call hasObject if method starts with 'get'
	    	if (substr($sMethodName, 0, 3) == 'has') {

	    		return $this -> hasObject(substr($sMethodName, 3));
	    	}

	    	// call hasObject if method starts with 'get'
	    	if (substr($sMethodName, 0, 6) == 'remove') {

	    		return $this -> removeObject(substr($sMethodName, 6));
	    	}

	    	// unknown method call -> throw exception
			throw new BadMethodCallException("The method '$sMethodName' does not exist on ". get_class($this) .".");
	    }


	    /**
	     * Set an object in the context and make it available under the given name.
	     *
	     * @param string $sName the name of the object
	     * @param mixed $objNewObject some kind of object (or primitive)
	     */
	    public function setObject($sName, $objNewObject) {

	    	switch (gettype($objNewObject)) {

	    		case 'object': $this -> objLogger -> info("Updating object '$sName' with an object '" . get_class($objNewObject) ."'."); break;
	    		case 'array': $this -> objLogger -> info("Updating object '$sName' with an array with " . count($objNewObject) ." elements."); break;
	    		default: $this -> objLogger -> info("Updating object '$sName' with " . gettype($objNewObject) ." '". $objNewObject ."'."); break;
	    	}

	    	$this -> arrObjects[$sName] = $objNewObject;
	    }


		/**
		 * Checks if there is an object under the given name in the context.
		 *
		 * @param string $sName the name to look for
		 * @return boolean returns true if there is an object registered for this name
		 */
	    public function hasObject($sName) {

	    	return array_key_exists($sName, $this -> arrObjects);
	    }


		/**
		 * Returns the object that is registered under the given name. If there is no
		 * matching object a ContextException is thrown.
		 *
		 * @param string $sName the name of the desired object
		 * @throws ContextException if there is no object with the given name
		 */
	    public function getObject($sName) {

	    	if ($this -> hasObject($sName)) {

	    		return $this -> arrObjects[$sName];
	    	}
	    	else {

	    		throw new ContextException("An object with name '$sName' is not registered in the context.");
	    	}
	    }


	    /**
	     * Removes the specified object from the context.
	     *
	     * @param string $sName the object to remove. The existence of this object is not checked.
	     */
	    public function removeObject($sName) {

	    	unset($this -> arrObjects[$sName]);
	    	
	    	// remove possible initialization marker
	    	unset($this -> arrInitializedObjects[$sName]);
	    }


	    /**
	     * Returns all registered objects as associative array.
	     *
	     * @return array all objects
	     */
	    public function getAllObjects() {

	    	return $this -> arrObjects;
	    }
	    
	    
	    /**
	     * Calls special named setter methods on the provided object
	     * to set objects of this context.
	     * 
	     * Example:
	     * If the context contains an object named EventManager, the method
	     * will look for a method named setEventManager() and call that method
	     * with the current object of the context as parameter.
	     * 
	     * @param $objCOS the object to call the setters on
	     */
	    protected function supplyObjects(ContextObjectSupplied $objCOS) {
	    	
			// iterate all objects from the context
			foreach ($this -> arrObjects as $sName => $objTheObject) {

				$sMethodName = 'set'. $sName;
				if (method_exists($objCOS, $sMethodName)) {

					$objCOS -> $sMethodName($objTheObject);
					$this -> objLogger -> debug("Supplied context object '$sName' in object of type '". get_class($objCOS) ."'.");
				}
			}
	    }
	    
	    
	    /**
	     * Sets the application context (this object) at the given target
	     * object if the target object implements ApplicationContextAware.
	     * 
	     * Sets all context objects of this context at the given target
	     * object if the target object implements ContextObjectSupplied;
	     * 
	     * @param $objTarget the target object. If no object given nothing happens.
	     */
	    public function autowireObject($objTarget) {
	    	
	    	// ensure we have an object here
	    	if (! is_object($objTarget)) {
	    		
	    		return;
	    	}
	    	
	    	// supply the whole Context object
	    	if ($objTarget instanceof ApplicationContextAware) {
	    		
				$this -> objLogger -> debug("Supplied the application context in object of type '". get_class($objTarget) ."'.");
	    		$objTarget -> setApplicationContext($this);
	    	}
	    	
	    	// supply single objects of this context
	    	if ($objTarget instanceof ContextObjectSupplied) {
	    		
	    		$this -> supplyObjects($objTarget);
	    	}
	    }
	    
	    
	    /**
	     * Calls autowireObject() for every object in this context.
	     * Call that method whenever the context was updated and
	     * the update is finished (for the moment). Implicitly
	     * all not already initialized objects get initialized.
	     */
	    public function autowireContextObjects() {
	    	
	    	// autowire all objects
	    	foreach ($this -> arrObjects as $sName => $objTheObject) {
	    		
	    		$this -> autowireObject($objTheObject);
	    	}
	    	
	    	// call initialize() methods
	    	$this -> initializeObjects();
	    }
	    
	    
	    /**
	     * Initializes all objects that implement the ContextInitialized
	     * interface that were not already initialized.
	     */
	    public function initializeObjects() {
	    	
	    	foreach ($this -> arrObjects as $sName => $objTheObject) {
	    		
	    		if (! $this -> isObjectInitialized($sName) &&
	    		    $objTheObject instanceof ContextInitialized) {
	    			
					$this -> objLogger -> debug("Initializing context object '$sName'.");
	    		    	
					// call the init method
	    		    $objTheObject -> initialize();
	    		    
	    		    // mark object as initialized
	    		    $this -> arrInitializedObjects[$sName] = true;
	    		}
	    	}
	    }
	    
	    
	    /**
	     * Checks if the specified object was already initialized
	     * 
	     * @param $sName the name of the object
	     * @return true if the initialize() method of that object was called once.
	     */
	    public function isObjectInitialized($sName) {
	    	
	    	return isset($this -> arrInitializedObjects[$sName]);
	    }


	    /**
	     * Returns a human readable inventory of the context.
	     *
	     * @return string string representation
	     */
	    public function __toString() {

	    	$sOut = get_class($this) .": \n";
	    	foreach ($this -> arrObjects as $sName => $mValue) {

	    		$sOut .= "   $sName: ". FormattingUtil :: getReadable($mValue) ."\n";
	    	}

	    	return $sOut;
	    }
	}
